﻿namespace ImapComponent.Net.Mail.Mime
{
    using System;
    using System.Collections.Generic;
    using System.Collections.Specialized;
    using System.IO;
    using System.Net.Mail;
    using System.Net.Mime;
    using System.Text;
    using System.Text.RegularExpressions;

    using ImapComponent.Net.Mail.Imap4;
    using ImapComponent.Net.Mail.Utils;

    /// <summary>
    /// Represents the email message.
    /// </summary>
    public class EmailMessage
    {
        #region Fields

        private readonly Envelope envelope;
        private readonly NameValueCollection headers;
        private readonly List<MimeSinglepart> plainBodies;
        private readonly List<MimeSinglepart> htmlBodies;
        private readonly MimeAttachmentCollection attachments;
        private readonly bool isHtml;
        private readonly bool isValid;
        private MimePart mimePart;
        private string bodyHtml;
        private string bodyPlain;

        #endregion // Fields

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the EmailMessage class.
        /// </summary>
        /// <param name="envelope">A message envelope.</param>
        /// <param name="emailMessageString">An email message string.</param>
        public EmailMessage(Envelope envelope, string emailMessageString)
        {
            this.plainBodies = new List<MimeSinglepart>();
            this.htmlBodies = new List<MimeSinglepart>();
            this.attachments = new MimeAttachmentCollection();
            
            this.envelope = envelope;
            this.headers = new NameValueCollection();

            try
            {
                this.ParseMimePart(emailMessageString);
                this.SetPlainText();
                this.SetHtmlText();
                this.SetAttachments();

                if (this.plainBodies.Count > 0)
                {
                    if (!string.IsNullOrEmpty(this.plainBodies[0].ContentType.CharSet))
                    {
                        try
                        {
                            this.bodyPlain = Encoding.GetEncoding(this.plainBodies[0].ContentType.CharSet).GetString(this.plainBodies[0].ContentData);
                        }
                        catch
                        {
                            this.bodyPlain = Encoding.Default.GetString(this.plainBodies[0].ContentData);
                        }
                    }
                    else
                    {
                        this.bodyPlain = Encoding.Default.GetString(this.plainBodies[0].ContentData);
                    }
                }
                else
                {
                    this.bodyPlain = string.Empty;
                }

                if (this.htmlBodies.Count > 0)
                {
                    if (!string.IsNullOrEmpty(this.htmlBodies[0].ContentType.CharSet))
                    {
                        try
                        {
                            this.bodyHtml = Encoding.GetEncoding(this.htmlBodies[0].ContentType.CharSet).GetString(this.htmlBodies[0].ContentData);
                        }
                        catch
                        {
                            this.bodyHtml = Encoding.Default.GetString(this.htmlBodies[0].ContentData);
                        }
                    }
                    else
                    {
                        this.bodyHtml = Encoding.Default.GetString(this.htmlBodies[0].ContentData);
                    }
                }
                else
                {
                    this.bodyHtml = string.Empty;
                }

                this.isHtml = !string.IsNullOrEmpty(this.bodyHtml);

                this.isValid = this.envelope.IsValid;
            }
            catch
            {
                this.isValid = false;
            }
        }

        #endregion // Constructors

        #region Properties

        /// <summary>
        /// Gets the from email address.
        /// </summary>
        public MailAddress From
        {
            get
            {
                return this.envelope.From;
            }
        }

        /// <summary>
        /// Gets the replyTo email address.
        /// </summary>
        public MailAddress ReplyTo
        {
            get
            {
                return this.envelope.ReplyTo;
            }
        }

        /// <summary>
        /// Gets the sender email address.
        /// </summary>
        public MailAddress Sender
        {
            get
            {
                return this.envelope.Sender;
            }
        }

        /// <summary>
        /// Gets the address collection that contains the blind carbon copy (BCC) recipients for this e-mail message. 
        /// </summary>
        public MailAddressCollection Bcc
        {
            get
            {
                return this.envelope.Bcc;
            }
        }

        /// <summary>
        /// Gets the address collection that contains the carbon copy (CC) recipients for this e-mail message.
        /// </summary>
        public MailAddressCollection Cc
        {
            get
            {
                return this.envelope.Cc;
            }
        }

        /// <summary>
        /// Gets the address collection that contains the recipients of this e-mail message.
        /// </summary>
        public MailAddressCollection To
        {
            get
            {
                return this.envelope.To;
            }
        }

        /// <summary>
        /// Gets the date when the message was composed.
        /// </summary>
        public DateTime Date
        {
            get
            {
                return this.envelope.Date;
            }
        }

        /// <summary>
        /// Gets the date when the message was received.
        /// </summary>
        public DateTime DateReceived
        {
            get
            {
                return this.envelope.DateReceived;
            }
        }

        /// <summary>
        /// Gets the message flags.
        /// </summary>
        public MessageFlagSet Flags
        {
            get
            {
                return this.envelope.Flags;
            }
        }

        /// <summary>
        /// Gets the InReplyTo value.
        /// </summary>
        public string InReplyTo
        {
            get
            {
                return this.envelope.InReplyTo;
            }
        }

        /// <summary>
        /// Gets the message id.
        /// </summary>
        public string MessageId
        {
            get
            {
                return this.envelope.MessageId;
            }
        }

        /// <summary>
        /// Gets or sets the subject line for this e-mail message.
        /// </summary>
        public string Subject
        {
            get
            {
                return this.envelope.Subject;
            }
        }

        /// <summary>
        /// Gets the message size.
        /// </summary>
        public int Size
        {
            get
            {
                return this.envelope.Size;
            }
        }

        /// <summary>
        /// Gets the message number.
        /// </summary>
        public int MessageNumber
        {
            get
            {
                return this.envelope.MessageNumber;
            }
        }

        /// <summary>
        /// Gets the message uid.
        /// </summary>
        public int Uid
        {
            get
            {
                return this.envelope.Uid;
            }
        }

        /// <summary>
        /// Gets the email message's headers.
        /// </summary>
        public NameValueCollection Headers
        {
            get
            {
                return this.headers;
            }
        }

        /// <summary>
        /// Gets the HTML body of the message.
        /// </summary>
        public string BodyHtml
        {
            get
            {
                return this.bodyHtml;
            }
        }

        /// <summary>
        /// Gets the plain text body of the message.
        /// </summary>
        public string BodyPlain
        {
            get
            {
                return this.bodyPlain;
            }
        }

        /// <summary>
        /// Gets the main mime part.
        /// </summary>
        public MimePart MimePart
        {
            get
            {
                return this.mimePart;
            }
        }

        /// <summary>
        /// Gets the plain parts of the message.
        /// </summary>
        public List<MimeSinglepart> PlainBodies
        {
            get
            {
                return this.plainBodies;
            }
        }

        /// <summary>
        /// Gets the html parts of the message.
        /// </summary>
        public List<MimeSinglepart> HtmlBodies
        {
            get
            {
                return this.htmlBodies;
            }
        }

        /// <summary>
        /// Gets the attachment collection.
        /// </summary>
        public MimeAttachmentCollection Attachments
        {
            get
            {
                return this.attachments;
            }
        }

        /// <summary>
        /// Gets a value indicating if there is some html body of the message.
        /// </summary>
        public bool IsHtml
        {
            get
            {
                return this.isHtml;
            }
        }

        /// <summary>
        /// Gets a value indicating whether the email message was successfuly parsed or not.
        /// </summary>
        public bool IsValid
        {
            get
            {
                return this.isValid;
            }
        }

        #endregion // Properties

        #region Methods

        /// <summary>
        /// Parses headers of the email message.
        /// </summary>
        /// <param name="emailMessageString">An email message string.</param>
        /// <returns>The message body string.</returns>
        private string ParseHeaders(string emailMessageString)
        {
            StringReader stringReader = new StringReader(emailMessageString);
            string line = stringReader.ReadLine();

            while (line != null)
            {
                if (line == string.Empty)
                {
                    break;
                }

                string headerLine = line;
                line = stringReader.ReadLine();

                while (line != null && (line.StartsWith("\t") || line.StartsWith(" ")))
                {
                    headerLine += line;
                    line = stringReader.ReadLine();
                }

                string[] headerParsedLine = headerLine.Split(new char[] { ':' }, 2);

                if (headerParsedLine.Length == 2)
                {
                    this.headers.Add(headerParsedLine[0].ToLower(), StringEncoder.DecodeHeaderValue(headerParsedLine[1].Trim()).ToLower());
                }
            }

            return stringReader.ReadToEnd();
        }

        /// <summary>
        /// Parses the mime part.
        /// </summary>
        /// <param name="emailMessageString">An email message string.</param>
        private void ParseMimePart(string emailMessageString)
        {
            string messageBodyString = this.ParseHeaders(emailMessageString);

            string contentId = this.headers.AllKeys.Contains("content-id") ? this.headers.GetValues("content-id")[0] : string.Empty;
            string transferEncodingString = this.headers.AllKeys.Contains("content-transfer-encoding") ? this.headers.GetValues("content-transfer-encoding")[0] : string.Empty;

            TransferEncoding transferEncoding;

            switch (transferEncodingString.ToLower())
            {
                case "":
                    transferEncoding = TransferEncoding.Unknown;
                    break;
                case "base64":
                    transferEncoding = TransferEncoding.Base64;
                    break;
                case "quoted-printable":
                    transferEncoding = TransferEncoding.QuotedPrintable;
                    break;
                case "7bit":
                    transferEncoding = TransferEncoding.SevenBit;
                    break;
                default:
                    transferEncoding = TransferEncoding.Unknown;
                    break;
            }

            string contentTypeString = this.headers.AllKeys.Contains("content-type") ? Regex.Replace(this.headers.GetValues("content-type")[0], "\r\n", string.Empty) : "text/plain;charset=\"utf-8\"";
            ContentType contentType = Helper.GetContentType(contentTypeString);

            /// multipart
            if (contentType.MediaType.ToLower().StartsWith("multipart"))
            {
                if (string.IsNullOrEmpty(contentType.Boundary))
                {
                    throw new InvalidDataException("Not a mime message.");
                }
                else
                {
                    StringReader stringReader = new StringReader(messageBodyString);
                    StringBuilder stringBuilder = new StringBuilder();

                    string line = stringReader.ReadLine();

                    while (line != null)
                    {
                        if (line.ToLower().StartsWith("--" + contentType.Boundary))
                        {
                            break;
                        }

                        stringBuilder.AppendLine(line);
                        line = stringReader.ReadLine();
                    }

                    string bodyString = stringBuilder.Length > 2 ? stringBuilder.ToString(0, stringBuilder.Length - 2) : string.Empty;
                    byte[] body = StringEncoder.DecodeData(transferEncoding, Encoding.ASCII.GetBytes(bodyString));

                    this.mimePart = new MimeMultipart(null, body, contentType, contentId, transferEncoding);

                    if (string.IsNullOrEmpty(line))
                    {
                        throw new InvalidDataException("Not a mime message.");
                    }

                    while (true)
                    {
                        if (line.ToLower().StartsWith("--" + contentType.Boundary + "--"))
                        {
                            break;
                        }

                        line = stringReader.ReadLine();
                        stringBuilder = new StringBuilder();

                        while (line != null)
                        {
                            if (line.ToLower().StartsWith("--" + contentType.Boundary))
                            {
                                break;
                            }

                            stringBuilder.AppendLine(line);
                            line = stringReader.ReadLine();
                        }

                        string mimePartString = stringBuilder.Length > 2 ? stringBuilder.ToString(0, stringBuilder.Length - 2) : string.Empty;

                        try
                        {
                            ((MimeMultipart)this.mimePart).Children.Add(MimePart.Parse(this.mimePart, mimePartString));
                        }
                        catch
                        {
                            /// skip invalid mime part
                        }
                    }
                }
            }
            /// mime attachment
            else if (this.headers.AllKeys.Contains("content-disposition"))
            {
                byte[] body = StringEncoder.DecodeData(transferEncoding, Encoding.ASCII.GetBytes(messageBodyString));
                string contentDispositionString = Regex.Replace(this.headers.GetValues("content-disposition")[0], "\r\n", string.Empty);
                ContentDisposition contentDisposition = Helper.GetContentDisposition(contentDispositionString);

                this.mimePart = new MimeAttachment(null, body, contentType, contentId, transferEncoding, contentType.Name, contentDisposition);

            }
            /// single part
            else
            {
                byte[] body = StringEncoder.DecodeData(transferEncoding, Encoding.ASCII.GetBytes(messageBodyString));

                this.mimePart = new MimeSinglepart(null, body, contentType, contentId, transferEncoding);
            }
        }

        /// <summary>
        /// Sets the plain text collection.
        /// </summary>
        private void SetPlainText()
        {
            if (this.mimePart is MimeSinglepart)
            {
                MimeSinglepart mimeSinglePart = this.mimePart as MimeSinglepart;

                if (mimeSinglePart.ContentType.MediaType.ToLower() == "text/plain")
                {
                    this.plainBodies.Add(mimeSinglePart);
                }
            }
            else if (this.mimePart is MimeMultipart)
            {
                MimeMultipart mimeMultiPart = this.mimePart as MimeMultipart;

                mimeMultiPart.SetPlainText(this.plainBodies);
            }
        }

        /// <summary>
        /// Sets the html text collection.
        /// </summary>
        private void SetHtmlText()
        {
            if (this.mimePart is MimeSinglepart)
            {
                MimeSinglepart mimeSinglePart = this.mimePart as MimeSinglepart;

                if (mimeSinglePart.ContentType.MediaType.ToLower() == "text/html")
                {
                    this.htmlBodies.Add(mimeSinglePart);
                }
            }
            else if (this.mimePart is MimeMultipart)
            {
                MimeMultipart mimeMultiPart = this.mimePart as MimeMultipart;

                mimeMultiPart.SetHtmlText(this.htmlBodies);
            }
        }

        /// <summary>
        /// Sets the attachment collection.
        /// </summary>
        private void SetAttachments()
        {
            if (this.mimePart is MimeAttachment)
            {
                MimeAttachment attachment = this.mimePart as MimeAttachment;

                this.attachments.Add(attachment);
            }
            else if (this.mimePart is MimeMultipart)
            {
                MimeMultipart mimeMultiPart = this.mimePart as MimeMultipart;

                mimeMultiPart.SetAttachments(this.attachments);
            }
        }

        #endregion // Methods
    }
}
